component accessors="true"
{
	// PRIVATE PROPERTIES
	
	property name="beanUtils";
	
	/**
	 * Lookup table of maps:  keyed by [source][target]
	 */
	property name="mapTable";
	
	// PUBLIC PROPERTIES
	
	/**
	 * The IMetadataBuilder to use to build metadata about mapped classes.  Defaults
	 * to the DefaultMetadataBuilder.
	 */
	property name="metadataBuilder";
	
		
	/**
	 * The IGraphedInstanceFactory to use when mapping operations need to create new 
	 * instances of a given class encountered while performing a mapping operation.
	 */
	property name="graphedInstanceFactory";
	
	/**
	 * The IValueResolverFactory to use when creating value resolvers for mapped properties.
	 * Defaults to the DefaultValueResolverFactory.
	 */
	property name="valueResolverFactory";
		
		
	import com.firemoss.atonement.mapper.error.*;
	import com.firemoss.atonement.mapper.resolution.*;
	import com.firemoss.atonement.metadata.*;
	
	/**
	 * Can be used to set mappings, all at once, as an array.  Useful when configuring
	 * via an IoC container such as the one provided by the ColdSpring framework.
	 */
	public void function setMappings( Array mappings )
	{
		var i = 0;
		
		for ( i=1; i<=arrayLen(mappings); i++ ) 
		{
			addMapping( mappings[i] );
		}
	}
	
	// CONSTRUCTOR
	
	public function init()
	{
		variables.mapTable = createObject( "java", "java.util.HashMap" ).init();
		variables.metadataBuilder = new DefaultMetadataBuilder();
		variables.graphedInstanceFactory = new DefaultGraphedInstanceFactory();
		variables.valueResolverFactory = new DefaultValueResolverFactory();
		variables.beanUtils = createObject( "component", "com.firemoss.atonement.metadata.beanutils.BeanUtils").init();
		resetMapTable();
	}
	
	// PUBLIC METHODS
	
	public any function map( source, target, mappingChain )
	{
		var sourceClass = getMetadata( source );
		var targetClass = getMetadata( target );
		var mapping = "";
		var resolver = "";
		var sourceValue = "";
		var key = "";
				
		if ( not mappingExistsBetweenByMetadata( sourceClass, targetClass ) )
		{
			createImplicitMapping( source, target );
		}
		
		if ( not structKeyExists( arguments, "mappingChain" ) )
		{
			mappingChain = createObject( "java", "java.util.HashMap" ).init();
		}
		
		mapping = getMappingFor( source, target );
		
		if ( !mapping.getValidated() )
		{
			mapping.assertMappingIsValid();
		}
		
		for ( key in mapping.getResolvedProperties() )
		{
			resolver = mapping.getResolvedProperties()[ key ];
			
			if ( not isNull( resolver.getSourceProperty() ) )
			{
				sourceValue = beanUtils.getProperty(source, resolver.getSourceProperty().getName() ); 
			}
			else
			{
				sourceValue = source;
			}
			
			if ( not isNull( sourceValue ) )
			{
				var resolvedValue = Evaluate("resolver.getValueResolutionTarget().#resolver.getValueResolutionFunction()#( sourceValue, resolver.getTargetProperty(), mapping, mappingChain )");
				
				if ( not isNull( resolvedValue ) )
				{
					beanUtils.setProperty( target, resolver.getTargetProperty().getName(), resolvedValue);
				}
				else
				{
					beanUtils.setProperty( target, resolver.getTargetProperty().getName(), javaCast( "null", ""));
				}
			}
			else
			{
					beanUtils.setProperty( target, resolver.getTargetProperty().getName(), javaCast( "null", ""));
			}
		}
		
		return target;
	}
	
	/**
	 * Creates a new Mapping for the given source and target and return the Mapper itself
	 */
	public function addMapping( mapping )
	{
		mapping.setMetadataBuilder( variables.metadataBuilder );
		mapping.setMapper( this );
		
		if ( isNull( mapping.getSource() ) || isNull( mapping.getTarget() ) )
		{
			throw new Error( "Can't add a mapping with a null source or target!" );
		}
		
		if ( not mapTable.containsKey( mapping.getSource() ) )
		{
			mapTable.put( mapping.getSource() , createObject( "java", "java.util.HashMap").init() );
		}
		
		mapTable.get( mapping.getSource() ).put( mapping.getTarget(), mapping );
		
		mapping.resolveProperties( valueResolverFactory );
			
		return this;
	}
	
	/**
	 * States if a mapping exists between two classes
	 */
	public boolean function mappingExistsBetween( source, target )
	{
		return mapTable.containsKey( getMetadata( source ) ) && mapTable.get( getMetadata( source ) ).containsKey( getMetadata( target ) );
	}
	
	/**
	 * States if a mapping exists between two classes
	 */
	public boolean function mappingExistsBetweenByMetadata( source, target )
	{
		return mapTable.containsKey( source ) && mapTable.get( source ).containsKey( target );
	}
	
	/**
	 * Gets a mapping between two classes
	 */
	public function getMappingFor( source , target )
	{
		if ( !mappingExistsBetween( source, target ) )
		{
			throw( "You've asked for a mapping between " & Type.forClass( source ).name + " and " & Type.forClass( source ).name & ", but none exists." );
		}
		
		return mapTable.get( getMetadata( source ) ).get( getMetadata( target ) );
	}
	
	/**
	 * Destroys all established maps.
	 */
	public function resetMapTable()
	{
		mapTable = createObject( "java", "java.util.HashMap" ).init();
	}
	
	/**
	 * Attempts to validate all mappings.  If an invalid mapping is found,
	 * its errors are printed to the console and assertion stops.  Originally,
	 * all errors from all mappings were printed, but the output was just overwhelming,
	 * even in small test cases.
	 */
	public function assertMappingsAreValid()
	{
		var name = [];
		var type = [];
		var source = "";
		var target = "";
		
		for ( source in mapTable )
		{
			for ( target in mapTable.get( source ) )
			{
				// Go ahead and stop on the first 
				mapTable.get( source ).get( target ).assertMappingIsValid();
			}
		}
		
	}
	
	private function createImplicitMapping( sourceClass, targetClass )
	{
		var mapping = new Mapping( sourceClass, targetClass );
		var ex = "";
		var invalidByType = "";
		var i = "";
		
		
		addMapping( mapping );
		
		try 
		{
				mapping.assertMappingIsValid();
		}
		catch(InvalidMappingsError e) 
		{
					ex = e;
		}
		
		if ( !isSimpleValue( ex ) )
		{
			var deserialized = deserializeJSON( ex.extendedInfo );
		
			for ( i=1; i<arrayLen( deserialized.byType); i++ )
			{
				//TODO: createImplicitMapping( invalidByType.getSourceProperty().getMetadata(), invalidByType.getTargetProperty().getMetadata() );
			}
		}

	}
}